#include "Application.h"
#include "../Rendering/Scene.h"
#include "../Utility/GameTimer.h"
#include "../Utility/PseudoRNG.h"

#include <iostream>
#include <string>
#include <vector>
#include <assert.h>
#include <glm\gtc\quaternion.hpp>

Application* gpApp = NULL;

/*
LRESULT CALLBACK WndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
{
	switch (uMsg)
	{
		case WM_CREATE:
		{
			return 0;
		}
	}

	// Don't start processing messages until after WM_CREATE.
	if( gpApp )
		return gpApp->MsgProc( hWnd, uMsg, wParam, lParam );
	else
		return DefWindowProc( hWnd, uMsg, wParam, lParam );
}
*/

Application::Application()
{
	gpApp = this;

	mpGame = NULL;
	mpScene = NULL;

	//pPRNG = new PseudoRandomNumberGenerator();

	logFile = std::ofstream("../Log/Debug_Log.txt");
	glLogFile = std::ofstream("../Log/Gl_Debug_Log.txt");
	clLogFile = std::ofstream("../Log/Cl_Debug_Log.txt");
}

Application::~Application()
{
	//delete pPRNG;

	glfwTerminate();

	if (clLogFile.is_open())
		clLogFile.close();

	if (glLogFile.is_open())
		glLogFile.close();
}

static void error_callback(int error, const char* description)
{
	gpApp->PrintToClLog("GLFW Error Code: ");
	gpApp->PrintToClLog(error);
	gpApp->PrintToClLog('\n');
	gpApp->PrintToClLog(description);
	gpApp->PrintToClLog('\n');
	gpApp->PrintToClLog('\n');
}

static void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
	if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
		glfwSetWindowShouldClose(window, GL_TRUE);
}

bool Application::Initialize(HINSTANCE hInstance)
{
	// Initialize OpenGl
	glLogFile << "<---------- Initializeing OpenGL ----------> \n";
	glfwSetErrorCallback(error_callback);
	if (!glfwInit())
		return false;

	glfwWindowHint(GLFW_SAMPLES, 4); // 4x antialiasing
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); // We want OpenGL 4.3
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); //We don't want the old OpenGL

	// Initialize Window
	mainWindow = glfwCreateWindow(640, 480, vGetGameTitle(), NULL, NULL);
	if (!mainWindow)
	{
		glfwTerminate();
		return false;
	}

	glfwMakeContextCurrent(mainWindow);

	// Initialize GLEW
	glewExperimental = true; // Needed in core profile
	if (glewInit() != GLEW_OK)
	{
		glLogFile << stderr << "Failed to initialize GLEW\n";
		return false;
	}

	glfwSetKeyCallback(mainWindow, keyCallback);

	glfwGetFramebufferSize(mainWindow, &windowWidth, &windowHeight);
	glViewport(0, 0, windowWidth, windowHeight);

	glEnable(GL_DEPTH_TEST);
	glDepthMask(GL_TRUE);
	glDepthFunc(GL_LEQUAL);
	glDepthRange(0.0f, 1.0f);

	glLogFile << "OpenGL initialization successful" << '\n';
	glLogFile << '\n';

	// Initialize Opencl
	cl_int error;
	clLogFile << "<---------- Initializeing OpenCL ----------> \n";

	cl_uint numPlatforms;
	clGetPlatformIDs(5, NULL, &numPlatforms);

	cl_platform_id* platforms = (cl_platform_id*)malloc(sizeof(cl_platform_id)* numPlatforms);
	clGetPlatformIDs(numPlatforms, platforms, NULL);

	clLogFile << "<---------- OpenCl Platform Information ----------> \n";
	for (unsigned int i = 0; i < numPlatforms; i++)
	{
		char platformInfo[48];
		clGetPlatformInfo(platforms[i], CL_PLATFORM_NAME, sizeof(platformInfo), &platformInfo, NULL);
		clLogFile << "Platform Name: " << platformInfo << '\n';

		clGetPlatformInfo(platforms[i], CL_PLATFORM_VENDOR, sizeof(platformInfo), &platformInfo, NULL);
		clLogFile << "Platform Vendor: " << platformInfo << '\n';

		clGetPlatformInfo(platforms[i], CL_PLATFORM_VERSION, sizeof(platformInfo), &platformInfo, NULL);
		clLogFile << "Platform Version: " << platformInfo << '\n';

		clGetPlatformInfo(platforms[i], CL_PLATFORM_PROFILE, sizeof(platformInfo), &platformInfo, NULL);
		clLogFile << "Platform Profile: " << platformInfo << '\n';

		size_t extSize;
		clGetPlatformInfo(platforms[i], CL_PLATFORM_EXTENSIONS, 0, NULL, &extSize);
		char* extData = (char*)malloc(extSize);
		clGetPlatformInfo(platforms[i], CL_PLATFORM_EXTENSIONS, extSize, extData, NULL);

		clLogFile << "Platform Extensions: " << extData << '\n' << '\n';
	}

	// I need to change this code to work with multiple platforms.
	cl_uint numDevices;
	error = clGetDeviceIDs(platforms[0], CL_DEVICE_TYPE_ALL, 0, NULL, &numDevices);

	mclDevices = (cl_device_id*)malloc(sizeof(cl_device_id)* numDevices);
	clGetDeviceIDs(platforms[0], CL_DEVICE_TYPE_ALL, numDevices, mclDevices, NULL);

	clLogFile << "<---------- OpenCl Device Information ----------> \n";
	for (unsigned int i = 0; i < numDevices; i++)
	{
		char deviceInfo[4096];
		error = clGetDeviceInfo(mclDevices[i], CL_DEVICE_NAME, sizeof(deviceInfo), deviceInfo, NULL); // for some reason this doesn't work.
		clLogFile << "Device Name: " << deviceInfo << '\n';

		clGetDeviceInfo(mclDevices[i], CL_DEVICE_VENDOR, sizeof(deviceInfo), &deviceInfo, NULL);
		clLogFile << "Device Vendor: " << deviceInfo << '\n';

		clGetDeviceInfo(mclDevices[i], CL_DEVICE_EXTENSIONS, sizeof(deviceInfo), &deviceInfo, NULL);
		clLogFile << "Device Extensions: " << deviceInfo << '\n';

		cl_ulong memSize;
		clGetDeviceInfo(mclDevices[i], CL_DEVICE_GLOBAL_MEM_SIZE, sizeof(memSize), &memSize, NULL);
		clLogFile << "Device Global Memory Size: " << memSize << '\n';

		cl_uint addrBits;
		clGetDeviceInfo(mclDevices[i], CL_DEVICE_ADDRESS_BITS, sizeof(addrBits), &addrBits, NULL);
		clLogFile << "Device Address Bits: " << addrBits << '\n';

		cl_bool clbool;
		clGetDeviceInfo(mclDevices[i], CL_DEVICE_AVAILABLE, sizeof(clbool), &clbool, NULL);
		clLogFile << "Device Available: " << clbool << '\n';

		clGetDeviceInfo(mclDevices[i], CL_DEVICE_COMPILER_AVAILABLE, sizeof(clbool), &clbool, NULL);
		clLogFile << "Device Compiler Available: " << clbool << '\n' << '\n';
	}
	
	cl_context_properties context_props[] =
	{
		CL_CONTEXT_PLATFORM, (cl_context_properties)platforms[0],
		CL_GL_CONTEXT_KHR, (cl_context_properties)wglGetCurrentContext(),
		CL_WGL_HDC_KHR, (cl_context_properties)wglGetCurrentDC(),
		0
	};

	mclContext = clCreateContextFromType(context_props, CL_DEVICE_TYPE_ALL, NULL, NULL, &error);

	FILE* programHandle = fopen("../Files/matrixmultiply.cl", "rb");
	fseek(programHandle, 0, SEEK_END);
	size_t programSize = ftell(programHandle);
	rewind(programHandle);
	char* programBuffer = (char*)malloc(programSize+1);
	programBuffer[programSize] = '\0';
	fread(programBuffer, sizeof(char), programSize, programHandle);
	fclose(programHandle);
	mclProgram = clCreateProgramWithSource(mclContext, 1, (const char**)&programBuffer, &programSize, &error);
	//clLogFile << "Loading CL Kernels: " << error << '\n';
	free(programBuffer);

	const char options[] = "-cl-std=CL1.1 -cl-mad-enable -Werror";
	clLogFile << "Compiling with options:  " << options << '\n';

	error = clBuildProgram(mclProgram, 1, &mclDevices[0], options, NULL, NULL);
	//clLogFile << "Compilation Result: " << error << '\n';

	char info[4096]; cl_uint ndevices;
	clGetProgramInfo(mclProgram, CL_PROGRAM_CONTEXT, sizeof(info), info, NULL);
	clLogFile << "Compiled for Program Context: " << info << '\n';
	clGetProgramInfo(mclProgram, CL_PROGRAM_NUM_DEVICES, sizeof(ndevices), &ndevices, NULL);
	clLogFile << "Compiled for Number of Devices: " << ndevices << '\n';

	std::ofstream clBuildLog = std::ofstream("../Log/Cl_Build_Log.txt");
	size_t bldLogSize;
	error = clGetProgramBuildInfo(mclProgram, mclDevices[0], CL_PROGRAM_BUILD_LOG, 0, NULL, &bldLogSize);
	char* bldLog = (char*)calloc(bldLogSize + 1, sizeof(char));
	error = clGetProgramBuildInfo(mclProgram, mclDevices[0], CL_PROGRAM_BUILD_LOG, bldLogSize, bldLog, NULL);
	clBuildLog << "Compiler Log: " << bldLog;
	free(bldLog);

	mclgpuQueue = clCreateCommandQueue(mclContext, mclDevices[0], CL_QUEUE_PROFILING_ENABLE, &error);
	mclcpuQueue = clCreateCommandQueue(mclContext, mclDevices[1], CL_QUEUE_PROFILING_ENABLE, &error);

	// Initialize Miscellaneous
	//pPRNG->initializeGenerator();

	// Initialize Scene
	mpScene = new Scene();
	mpScene->Initialize();

	// Initialize Game Logic
	mpGame = vCreateGame();
	mpGame->Initialize();

	// Initialize Timer
	gpTimer.reset(new GameTimer);

	return true;
}

void Application::OnUpdate( double dTime, double dElapsedTime )
{
	mpScene->Update(dTime, dElapsedTime);
}

void Application::OnRender()
{
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	glClearDepth(1.0f);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	mpScene->Render();

	// Finished.
	glfwSwapBuffers(mainWindow);
}